package net.kosev.battleships.model;

import java.io.Serializable;

import net.kosev.battleships.model.creators.BoardCreator;


/**
 * Board class. Implements all game logic like firing, hitting, missing
 * 
 * @author Nikola
 *
 */
public class Board implements Serializable {
	
	/**
	 * Serialization id
	 */
	private static final long serialVersionUID = 4049728953696434338L;

	/**
	 * The size of the board; board is square 10x10
	 */
	public static final int SIZE = 10;

	/**
	 * Square matrix representing the board
	 */
	private int[][] fields = null;
	
	/**
	 * Remaining ship parts (lives)
	 */
	private int remainingLives = 0;
	
	/**
	 * Total ship parts in the beginning of the game
	 */
	private int totalLives = 0;
	
	/**
	 * Count of total moves
	 */
	private int countMoves = 0;
	
	/**
	 * Count of hit moves
	 */
	private int countHits = 0;
	
	/**
	 * Board Listener (Observable pattern)
	 */
	private BoardListener listener = null;
	
	/**
	 * Is rule to reveal destroyed ships enabled
	 */
	private boolean ruleRevealing = true;
	
	/**
	 * Private constructor; board can be created only from specific static method
	 */
	private Board() { }
	
	/**
	 * Static method for creating boards
	 * 
	 * @param creator	BoardCreator instance that will arrange ships in board fields
	 * 
	 * @return	Board with complete ships on it
	 * 
	 * @throws BoardException Malformed board
	 */
	public static Board create(BoardCreator creator) throws BoardException {
		Board board = new Board();
		
		board.fields = new int[SIZE][SIZE];
		
		for (int i = 0; i < SIZE; i++) {
			for (int j = 0; j < SIZE; j++) {
				board.fields[i][j] = Field.WATER;
			}
		}
		
		creator.setFields(board.fields);
		creator.create();
		
		for (int i = 0; i < SIZE; i++) {
			for (int j = 0; j < SIZE; j++) {
				if (board.fields[i][j] != Field.WATER && board.fields[i][j] != Field.SHIP) {
					throw new BoardException("Fresh-created board cannot have hits and misses");
				}
				
				if (board.fields[i][j] == Field.SHIP) {
					board.remainingLives++;
				}
			}
		}
		
		board.totalLives = board.remainingLives;
		
		return board;
	}
	
	/**
	 * Perform fire move on the board; execute listener handlers on hit and miss
	 * 
	 * @param row		Board row to fire
	 * @param column	Board column to fire
	 * 
	 * @return True on hit, false on miss
	 * 
	 * @throws BoardException Invalid fire location
	 */
	public boolean fire(int row, int column) throws BoardException {
		if (row < 0 || row >= SIZE || column < 0 || column >= SIZE) {
			throw new BoardException("Invalid fire coordinates [" + row + ", " + column + "]");
		}
		if (remainingLives == 0) {
			throw new BoardException("Cannot fire on destroyed board");
		}
		if (fields[row][column] != Field.WATER && fields[row][column] != Field.SHIP) {
			throw new BoardException("Cannot fire on fired field [" + row + ", " + column + "]");
		}
		
		boolean result = false;
		
		if (fields[row][column] == Field.WATER) {
			
			fields[row][column] = Field.MISS;
			
		} else if (fields[row][column] == Field.SHIP) {
			
			fields[row][column] = Field.HIT;
			result = true;
			
			if (ruleRevealing == true) {
				temp = new int[SIZE][SIZE];
				if (checkForDestroyedShip(row, column) == true) {
					temp = new int[SIZE][SIZE];
					sunkShip(row, column);
				}
				temp = null;
			}
			
			remainingLives--;
			countHits++;
			
			if (remainingLives == 0 && listener != null) {
				listener.boardDestroyed(this);
			}
		}
		
		countMoves++;
		
		return result;
	}
	
	/**
	 * Temporary board used for recursion (revealing destroyed ships)
	 */
	private int[][] temp;
	
	/**
	 * Check if whole ship is destroyed
	 * 
	 * @param row	Row on board to fire
	 * @param col	Column on board to fire
	 * @return	Returns true if whole ship is destroyed
	 */
	private boolean checkForDestroyedShip(final int row, final int col) {
		if (row < 0 || row >= SIZE || col < 0 || col >= SIZE) {
			return true;
		}
		
		if (temp[row][col] == 1) {
			return true;
		}
		temp[row][col] = 1;
		
		if (fields[row][col] == Field.WATER || fields[row][col] == Field.MISS) {
			return true;
		}
		else if (fields[row][col] == Field.HIT) {
			if (checkForDestroyedShip(row - 1, col) == false) {
				return false;
			}
			if (checkForDestroyedShip(row, col + 1) == false) {
				return false;
			}
			if (checkForDestroyedShip(row + 1, col) == false) {
				return false;
			}
			if (checkForDestroyedShip(row, col - 1) == false) {
				return false;
			}
		}
		else if (fields[row][col] == Field.SHIP) {
			return false;
		}
		
		return true;
	}
	
	/**
	 * Check all field near sunk ship
	 * 
	 * @param row	Row in board to check
	 * @param col	Column in board to check
	 * 
	 * @throws BoardException	Exception on error
	 */
	private void sunkShip(final int row, final int col) throws BoardException {
		if (row < 0 || row >= SIZE || col < 0 || col >= SIZE) {
			return;
		}
		
		if (temp[row][col] == 1) {
			return;
		}
		temp[row][col] = 1;
		
		if (fields[row][col] == Field.WATER) {
			fields[row][col] = Field.MISS;
			return;
		}
		else if (fields[row][col] == Field.MISS) {
			return;
		}
		else if (fields[row][col] == Field.HIT) {
			sunkShip(row - 1, col);
			sunkShip(row - 1, col + 1);
			sunkShip(row, col + 1);
			sunkShip(row + 1, col + 1);
			sunkShip(row + 1, col);
			sunkShip(row + 1, col - 1);
			sunkShip(row, col - 1);
			sunkShip(row - 1, col - 1);
		}
		else if (fields[row][col] == Field.SHIP) {
			throw new BoardException("Error while sunking ship");
		}
	}

	/**
	 * Gets remaining ship parts
	 * 
	 * @return	Remaining ship parts
	 */
	public int getRemainingLives() {
		return remainingLives;
	}
	
	/**
	 * Gets total ship parts
	 * 
	 * @return	Total ship parts
	 */
	public int getTotalLives() {
		return totalLives;
	}
	
	/**
	 * Gets board fields
	 * 
	 * @return	Board fields
	 */
	public int[][] getFields() {
		return fields;
	}
	
	/**
	 * Sets board fields
	 * 
	 * @param fields	Fields to set into board
	 */
	public void setFields(int[][] fields) {
		this.fields = fields;
	}
	
	/**
	 * Sets board listener
	 * 
	 * @param listener	BoardListener instance
	 */
	public void setBoardListener(BoardListener listener) {
		this.listener = listener;
	}
	
	/**
	 * Get total moves
	 * 
	 * @return	Count of total moves
	 */
	public int getCountMoves() {
		return countMoves;
	}
	
	/**
	 * Get total hit moves
	 * 
	 * @return	Count of hit moves
	 */
	public int getCountHits() {
		return countHits;
	}
	
	/**
	 * Sets revealing rule
	 * 
	 * @param revealing Revealing is true or false
	 */
	public void setRuleRevealing(boolean revealing) {
		ruleRevealing = revealing;
	}

}
